10.3 左值与右值

简介

C++中的表达式要么是左值要么是右值,在C语言中可以位于赋值语句左侧的是左值,不能的被称为右值。但是在C++中两者的区别没有那么明显,但是可以简单归纳为:

  • 左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象

  • 当一个对象被用作右值的时候,用的是对象的值(内容);当一个对象被用作左值的时候,用的是对象的身份(在内存中的位置)

Tips:左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。。

运算符与左值右值

不同的运算符对运算对象的要求不尽相同:有的需要左值运算对象,有的需要右值运算对象;有的返回值得到左值结果,有的得到右值结果。

Tips:一个重要的原则是:在需要右值的地方可以用左值代替,但是不能把右值当做左值(也就是位置)来使用。

下面列举几种需要用到左值的常用运算符:

  • 赋值运算符:需要一个非常量左值作为左侧运算对象,返回一个左值

  • 取地址符:作用于一个左值运算对象,返回一个指向该运算对象的指针右值

  • 内置解引用运算符、下标运算符、迭代器解引用运算符、vector和string的下标运算符:求值结果都是左值

  • 内置类型和迭代器的递增递减运算符:作用于左值运算对象,其前置版本所得的结果也是左值

decltype与左值右值

如果表达式的求值结果是左值,那么关键字decltype作用于该表达式(不是变量)会得到一个引用类型。举个例子:

int *pi;
int a;
decltype(*pi) x = a;  // 解引用符生成左值, 因此decltype(*pi)结果是int&, 引用类型必须初始化
decltype(&pi) y;      // 取地址符生成右值, 因此decltype(&p)的结果是int**

右值引用

右值引用指的是必须绑定到右值的引用,我们可以通过&&来获取右值引用。右值引用一个重要的性质就是只能绑定到一个将要销毁的对象,因此我们可以自由地讲一个右值引用的资源“移动”到另一个对象中去。

Tips:正常来说左值引用只能绑定到左值上,右值引用只能绑定到右值上,但是我们可以将一个const的左值引用绑定到一个右值上。

int i = 42;
int &r = i;              // 正确: 左值引用
int &&rr = i;            // 错误: 不能将一个右值引用绑定到一个左值上
int &r2 = i * 42;        // 错误: 不能讲一个左值引用绑定到一个右值上
const int &r3 = i * 42;  // 正确: 我们可以将一个const的左值引用绑定到右值上
int &&rr2 = i * 42;      // 正确: 右值引用

需要注意的是我们不能将一个右值引用绑定到一个右值引用类型的变量上:

// 正确: 字面常量是右值, 可以被右值引用
int &&rr1 = 42;
// 错误: 变量是左值, 我们不能讲一个右值引用绑定到一个变量上, 即使这个变量是右值引用类型也不行
int &&rr2 = rr1; 

1. 标准库move函数获得右值引用

虽然我们不能将一个右值引用直接绑定到左值上,但我们可以使用utility头文件中的std::move函数来获得绑定到左值上的右值引用:

int &&rr1 = 42;
int &&rr2 = std::move(rr1);

需要注意的是:

  • 使用move的代码应该明确使用std::move而不是在提供using声明后使用move函数,防止和用户程序定义的接受单一形参的move函数冲突

  • 调用std::move就意味着承诺除了对原来的左值对象赋值或销毁它外,我们将不再使用这个对象的值

2. 接受右值引用参数的成员函数

除了构造函数和赋值运算符外,一个成员函数也可以同时提供拷贝和移动版本(一个版本接受指向const的左值引用,一个版本指向非const的右值引用):

// 定义push_back的标准库容器提供两个版本: 右值引用参数和const左值引用参数
void push_back(const T&);  // 拷贝: 绑定到任意类型的T
void push_back(T&&);       // 移动: 只能绑定到类型T的可修改的右值

Tips:一般情况下我们不需要为函数定义接受一个const T&&或是一个普通的T&参数的版本。因为当我们希望从实参“窃取”数据时,通常传一个右值引用参数且该实参不能是const的,当我们希望从一个对象拷贝数据时,通常不需要定义一个接受普通T&参数的版本。

3. 成员函数的右值引用限定符

通常情况下我们在一个对象上调用成员函数不需要管该对象是一个左值还是右值,比如:

string s1 = "tomo", s2 = "cat";
auto n = (s1 + s2).find('a');  // 在一个string右值上调用find成员
s1 + s2 = "test";              // 对一个string右值赋值

在旧标准中我们无法阻止上述这种使用方式,为了维持向后兼容性,新标准库类仍然允许向右值赋值。但是在新标准中如果我们想在自定义的类中阻止这种用法,可以强制左侧运算对象(即this指针指向的对象)是一个左值。

class Foo {
 public:
    // 左值引用限定符: 拷贝赋值运算符只可向可修改的左值赋值
    Foo &operator=(const Foo &rhs) & {
        return *this;
    }
};

Foo a, b;
a = b;             // 正确
std::move(a) = b;  // 错误: 不可向右值赋值

和左值引用限定符用法相似,右值引用限定符&&表示该成员函数只可应用于右值。我们可以综合引用限定符和const限定符来区分一个成员函数的重载版本:

class Foo {
 public:
    // 用于类型Foo的可改变的右值: 提高排序性能
    Foo sorted() && {
        // 本对象为右值, 可以原址排序
        std::sort(data_.begin(), data_.end());
        return *this;
    }
    // 可用于任何类型的Foo
    Foo sorted() const & {
        // 本对象是const或者一个左值, 不能原址排序
        Foo ret(*this);
        std::sort(ret.data_.begin(), ret.data_.end());
        return ret;
    }
 private:
    std::vector<int> data_;
};